﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.GamerServices;
using System.ComponentModel;
using XNAProject.GameElements;

namespace XNAProject.PathFinder
{
    public struct ReportState
    //used to pass back UserState in reportprogress
    {
        public Ship shp;
        public List<Vector2> pth;
    }

    public interface IPathFinder
    //interface declaration so we can use it as a service
    {
        bool RegisterPathFindingRequest(Ship thisShip);
        //register a new request
    };

    public class PathFinder : GameComponent, IPathFinder
    //pathfinder class implements A*
    {
        private static int CompareByFScore(searchNode a, searchNode b)
        //to sort nodes according to FScore
        {
            if (a.Fcost < b.Fcost) return -1;
            else if (a.Fcost == b.Fcost) return 0;
            else return 1;
        }

        private BackgroundWorker worker;
        //backgroundworker for pathfinding engine so it will not block main thread

        private List<Ship> requests;
        //to store the pathfinding requests from objects
 
        private List<searchNode> openList;
        //open list for A* algorythm

        private searchNode[,] searchArea;
        //contains optimized searcharea which is updated by UpdateLevelData()

        private int searchAreaSizeX = 0;
        private int searchAreaSizeY = 0;
        //searchArea bounds so we do not get IndexOutOfRange exception

        private Level levelService;
        //to store levelservice

        public PathFinder(Game ourGame)
            : base(ourGame)
        //constructor
        {
            //set up worker
            worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.WorkerSupportsCancellation = true;
            worker.DoWork += DoPathFinding;
            worker.ProgressChanged += ReportPathFinding;
            worker.RunWorkerCompleted += PathFindingDone;

            //get level service
            levelService = (Level)ourGame.Services.GetService(typeof(Level));

            //set up requests
            requests = new List<Ship>();

            //set up open-list
            openList = new List<searchNode>();
        }

        private void UpdateLevelData()
        //recreates the search area based on current level passed to it
        {
            //create the optimized searcharea
            searchAreaSizeX = (int)levelService.size.X;
            searchAreaSizeY = (int)levelService.size.Y;
            searchArea = new searchNode[searchAreaSizeX, searchAreaSizeY];

            //generate map
            for (int i = 0; i < levelService.size.X; i++)
            {
                for (int j = 0; j < levelService.size.Y; j++)
                {
                    searchArea[i, j] = new searchNode(levelService[i, j]);
                }
            }
        }

        public bool RegisterPathFindingRequest(Ship thisShip)
        //register a path request
        {
            if (!Vector2.Equals(thisShip.hexaPosition, thisShip.moveTarget))
            {
                requests.Add(thisShip);
                if (!worker.IsBusy)
                {
                    worker.RunWorkerAsync();
                }
                return true;
            }
            return false;
        }

        private void DoPathFinding(object sender, DoWorkEventArgs evt)
        //main loop of backgroundworker
        {
            //get sender so we can use multiple backgroundworkers just in case
            BackgroundWorker _sender = sender as BackgroundWorker;

            //process each request in List<pathRequests>
            while (requests.Count > 0)
			{
                //refresh data
                UpdateLevelData();

                bool foundPath = false;
                List<Vector2> path = null;
			    Ship thisRequest = requests[0];
                //clear openlist since we are starting a new request
                openList.Clear();

                //add the starting hexa
                openList.Add(searchArea[(int)thisRequest.hexaPosition.X, (int)thisRequest.hexaPosition.Y]);

                #region Mainloop of A*

                do
                {
                    //sort list, may need binary heep to speed this up
                    //openList.Sort(CompareByFScore);
                    //current node is only position because we do not need more
                    int cx, cy;
                    if (openList.Count != 0)
                    {
                        //get top node cheapest
                        cx = openList[0].posX;
                        cy = openList[0].posY;
                        //add to closed list
                        searchArea[cx, cy].washere = true;
                        //remove from open list
                        openList.RemoveAt(0);
                        //reorder openlist
                        openList.Sort(CompareByFScore);

                        //check out neighbours
                        /*these are the six neighbours of one hexagon
                        (x - 1, y - 1 + (x % 2))
                        (x - 1, y     + (x % 2))
                        (x,     y - 1          )
                        (x,     y + 1          )
                        (x + 1, y - 1 + (x % 2))
                        (x + 1, y     + (x % 2))
                         */
                        //bounds

                        #region Neighbours

                        int x, y;
                        x = cx - 1;
                        y = cy - 1 + (cx % 2);
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }
                        x = cx - 1;
                        y = cy + (cx % 2);
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }
                        x = cx;
                        y = cy - 1;
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }
                        x = cx;
                        y = cy + 1;
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }
                        x = cx + 1;
                        y = cy - 1 + (cx % 2);
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }
                        x = cx + 1;
                        y = cy + (cx % 2);
                        if (x > -1 && y > -1 && x < searchAreaSizeX && y < searchAreaSizeY)
                        {
                            ProcessNode(cx, cy, x, y);
                        }

                        #endregion
                    }
                    else
                    {
                        //list is empty no path existing
                        break;
                    }

                    if (openList.Contains(searchArea[(int)thisRequest.moveTarget.X, (int)thisRequest.moveTarget.Y]))
                    {
                        //path is found
                        foundPath = true;
                        break;
                    }

                } while (true);

                #endregion

                //create path if there is one
                if (foundPath)
                {
                    path = new List<Vector2>();
                    Vector2 thisNode = thisRequest.moveTarget;
                    do
                    {
                        path.Add(thisNode);
                        thisNode = new Vector2(searchArea[(int)thisNode.X, (int)thisNode.Y].parentNodeposX, searchArea[(int)thisNode.X, (int)thisNode.Y].parentNodeposY);
                        
                    } while (!Vector2.Equals(thisNode, thisRequest.hexaPosition));
                    path.Add(thisRequest.hexaPosition);
                    path.Reverse();
                }

                //if we got here then we are done with this request so report it
                ReportState rep = new ReportState();
                rep.pth = path;
                rep.shp = thisRequest;
                _sender.ReportProgress(100, rep );
                requests.RemoveAt(0);
			}
        }

        private void ProcessNode(int cx, int cy, int x, int y)
        //process a node cx,cy currentnode positions, x,y node to process
        {
            //if not on closed list in which case we already looked at it and allowed to step here
            if (!searchArea[x, y].washere &&
                 searchArea[x, y].allowed)
            {
                //if not in openlist add to it
                if (!openList.Contains(searchArea[x, y]))
                {
                    //parent is current node
                    searchArea[x, y].parentNodeposX = cx;
                    searchArea[x, y].parentNodeposY = cy;
                    //estimate is 0 atm, score is parent score+selfcost
                    searchArea[x, y].HCost = CalculateHCost(x, y);
                    searchArea[x, y].GCost =
                        searchArea[cx, cy].GCost + searchArea[x, y].AddedGCost;

                    searchArea[x, y].Fcost =
                        searchArea[x, y].HCost +
                        searchArea[x, y].GCost;

                    //add to openlist
                    openList.Add(searchArea[x, y]);

                    //reorder openlist
                    openList.Sort(CompareByFScore);
                }
                else//if the node is on the open list maybe this is a better route
                {
                    //calc gcost
                    int tempGCost = searchArea[cx, cy].GCost + searchArea[x, y].AddedGCost;

                    //if cost is lower then this is a better route indeed
                    if (tempGCost < searchArea[x, y].GCost)
                    {
                        //parent is current node
                        searchArea[x, y].parentNodeposX = cx;
                        searchArea[x, y].parentNodeposY = cy;

                        //assign the better Gcost
                        searchArea[x, y].GCost = tempGCost;

                        //update Fcost
                        searchArea[x, y].Fcost =
                        searchArea[x, y].HCost +
                        searchArea[x, y].GCost;

                        //reorder openlist
                        openList.Sort(CompareByFScore);
                    }
                }
            }
        }

        private int CalculateHCost(int x, int y)
        //estimate to goal rough but totally enough here
        {
            int hcost = (int)Vector2.Distance(requests[0].moveTarget, new Vector2(x, y));
            return hcost*10;
        }

        private void PathFindingDone(object sender, RunWorkerCompletedEventArgs evt)
        //occurs when List<pathRequests> is empty or we got cancelled
        {
            //we finished all pathfindings or got cancelled
            //if cancelled or error we need to notify everyone that we can no longer provide valid path data
        }

        private void ReportPathFinding(object sender, ProgressChangedEventArgs evt)
        //progress changed lets see if we are done with a request
        {
            //progress changed
            //check if first item is completed
            Console.WriteLine("called");
            //success report back to reuqester->evt.ProgressPercentage as requesterhash
            ((ReportState)evt.UserState).shp.PathDone(((ReportState)evt.UserState).pth);
        }
    }
}
